#include "eosio.system.hpp"

#include <eosio/eosio.hpp>
#include <eosio/print.hpp>
#include <eosio/datastream.hpp>
#include <eosio/serialize.hpp>
#include <eosio/multi_index.hpp>
#include <eosio/privileged.hpp>
#include <eosio/transaction.hpp>

#include <eosio.token.hpp>


#include <cmath>
#include <map>

namespace eosiosystem {
using eosio::asset;
using eosio::indexed_by;
using eosio::const_mem_fun;
using eosio::print;
using eosio::permission_level;
using eosio::time_point_sec;
using std::map;
using std::pair;

static constexpr uint32_t refund_delay_sec = 3*24*3600;
static constexpr int64_t  ram_gift_bytes = 1400;

struct [[eosio::table, eosio::contract("eosio.system")]] user_resources {
   name          owner;
   asset         net_weight;
   asset         cpu_weight;
   int64_t       ram_bytes = 0;

   bool is_empty()const { return net_weight.amount == 0 && cpu_weight.amount == 0 && ram_bytes == 0; }
   uint64_t primary_key()const { return owner.value; }

   // explicit serialization macro is not necessary, used here only to improve compilation time
   EOSLIB_SERIALIZE( user_resources, (owner)(net_weight)(cpu_weight)(ram_bytes) )
};


/**
 *  Every user 'from' has a scope/table that uses every receipient 'to' as the primary key.
 */
struct [[eosio::table, eosio::contract("eosio.system")]] delegated_bandwidth {
   name          from;
   name          to;
   asset         net_weight;
   asset         cpu_weight;

   bool is_empty()const { return net_weight.amount == 0 && cpu_weight.amount == 0; }
   uint64_t  primary_key()const { return to.value; }

   // explicit serialization macro is not necessary, used here only to improve compilation time
   EOSLIB_SERIALIZE( delegated_bandwidth, (from)(to)(net_weight)(cpu_weight) )

};

struct [[eosio::table, eosio::contract("eosio.system")]] refund_request {
   name            owner;
   time_point_sec  request_time;
   eosio::asset    net_amount;
   eosio::asset    cpu_amount;

   bool is_empty()const { return net_amount.amount == 0 && cpu_amount.amount == 0; }
   uint64_t  primary_key()const { return owner.value; }

   // explicit serialization macro is not necessary, used here only to improve compilation time
   EOSLIB_SERIALIZE( refund_request, (owner)(request_time)(net_amount)(cpu_amount) )
};

/**
 *  These tables are designed to be constructed in the scope of the relevant user, this
 *  facilitates simpler API for per-user queries
 */
typedef eosio::multi_index< "userres"_n, user_resources >      user_resources_table;
typedef eosio::multi_index< "delband"_n, delegated_bandwidth > del_bandwidth_table;
typedef eosio::multi_index< "refunds"_n, refund_request >      refunds_table;



/**
 *  This action will buy an exact amount of ram and bill the payer the current market price.
 */
void system_contract::buyrambytes( name payer, name receiver, uint32_t bytes ) {

   auto itr = _rammarket.find(ramcore_symbol.raw());
   auto tmp = *itr;
   auto eosout = tmp.convert( asset(bytes, ram_symbol), core_symbol() );

   buyram( payer, receiver, eosout );
}


/**
 *  When buying ram the payer irreversiblly transfers quant to system contract and only
 *  the receiver may reclaim the tokens via the sellram action. The receiver pays for the
 *  storage of all database records associated with this action.
 *
 *  RAM is a scarce resource whose supply is defined by global properties max_ram_size. RAM is
 *  priced using the bancor algorithm such that price-per-byte with a constant reserve ratio of 100:1.
 */
void system_contract::buyram( name payer, name receiver, asset quant )
{
   require_auth( payer );
   update_ram_supply();

   check( quant.symbol == core_symbol(), "must buy ram with core token" );
   check( quant.amount > 0, "must purchase a positive amount" );

   auto fee = quant;
   fee.amount = ( fee.amount + 199 ) / 200; /// .5% fee (round up)
   // fee.amount cannot be 0 since that is only possible if quant.amount is 0 which is not allowed by the assert above.
   // If quant.amount == 1, then fee.amount == 1,
   // otherwise if quant.amount > 1, then 0 < fee.amount < quant.amount.
   auto quant_after_fee = quant;
   quant_after_fee.amount -= fee.amount;
   // quant_after_fee.amount should be > 0 if quant.amount > 1.
   // If quant.amount == 1, then quant_after_fee.amount == 0 and the next inline transfer will fail causing the buyram action to fail.

   INLINE_ACTION_SENDER(eosio::token, transfer)(
         token_account, { {payer, active_permission}, {ram_account, active_permission} },
         { payer, ram_account, quant_after_fee, std::string("buy ram") }
   );

   if( fee.amount > 0 ) {
      INLINE_ACTION_SENDER(eosio::token, transfer)(
            token_account, { {payer, active_permission} },
            { payer, ramfee_account, fee, std::string("ram fee") }
      );
      channel_to_rex( ramfee_account, fee );
   }

   int64_t bytes_out;

   const auto& market = _rammarket.get(ramcore_symbol.raw(), "ram market does not exist");
   _rammarket.modify( market, same_payer, [&]( auto& es ) {
      bytes_out = es.convert( quant_after_fee,  ram_symbol ).amount;
   });

   check( bytes_out > 0, "must reserve a positive amount" );

   _gstate.total_ram_bytes_reserved += uint64_t(bytes_out);
   _gstate.total_ram_stake          += quant_after_fee.amount;

   user_resources_table  userres( _self, receiver.value );
   auto res_itr = userres.find( receiver.value );
   if( res_itr ==  userres.end() ) {
      res_itr = userres.emplace( receiver, [&]( auto& res ) {
         res.owner = receiver;
         res.net_weight = asset( 0, core_symbol() );
         res.cpu_weight = asset( 0, core_symbol() );
         res.ram_bytes = bytes_out;
      });
   } else {
      userres.modify( res_itr, receiver, [&]( auto& res ) {
         res.ram_bytes += bytes_out;
      });
   }
   set_resource_limits( res_itr->owner, res_itr->ram_bytes + ram_gift_bytes, res_itr->net_weight.amount, res_itr->cpu_weight.amount );
}

/**
  *  The system contract now buys and sells RAM allocations at prevailing market prices.
  *  This may result in traders buying RAM today in anticipation of potential shortages
  *  tomorrow. Overall this will result in the market balancing the supply and demand
  *  for RAM over time.
  */
void system_contract::sellram( name account, int64_t bytes ) {
   require_auth( account );
   update_ram_supply();

   check( bytes > 0, "cannot sell negative byte" );

   user_resources_table  userres( _self, account.value );
   auto res_itr = userres.find( account.value );
   check( res_itr != userres.end(), "no resource row" );
   check( res_itr->ram_bytes >= bytes, "insufficient quota" );

   asset tokens_out;
   auto itr = _rammarket.find(ramcore_symbol.raw());
   _rammarket.modify( itr, same_payer, [&]( auto& es ) {
      /// the cast to int64_t of bytes is safe because we certify bytes is <= quota which is limited by prior purchases
      tokens_out = es.convert( asset(bytes, ram_symbol), core_symbol());
   });

   check( tokens_out.amount > 1, "token amount received from selling ram is too low" );

   _gstate.total_ram_bytes_reserved -= static_cast<decltype(_gstate.total_ram_bytes_reserved)>(bytes); // bytes > 0 is asserted above
   _gstate.total_ram_stake          -= tokens_out.amount;

   //// this shouldn't happen, but just in case it does we should prevent it
   check( _gstate.total_ram_stake >= 0, "error, attempt to unstake more tokens than previously staked" );

   userres.modify( res_itr, account, [&]( auto& res ) {
      res.ram_bytes -= bytes;
   });
   set_resource_limits( res_itr->owner, res_itr->ram_bytes + ram_gift_bytes, res_itr->net_weight.amount, res_itr->cpu_weight.amount );

   INLINE_ACTION_SENDER(eosio::token, transfer)(
         token_account, { {ram_account, active_permission}, {account, active_permission} },
         { ram_account, account, asset(tokens_out), std::string("sell ram") }
   );

   auto fee = ( tokens_out.amount + 199 ) / 200; /// .5% fee (round up)
   // since tokens_out.amount was asserted to be at least 2 earlier, fee.amount < tokens_out.amount
   if( fee > 0 ) {
      INLINE_ACTION_SENDER(eosio::token, transfer)(
            token_account, { {account, active_permission} },
            { account, ramfee_account, asset(fee, core_symbol()), std::string("sell ram fee") }
      );
      channel_to_rex( ramfee_account, asset(fee, core_symbol() ));
   }
}

void validate_b1_vesting( int64_t stake ) {
   const int64_t base_time = 1527811200; /// 2018-06-01, Friday, June 1, 2018 12:00:00 AM UTC
   const int64_t max_claimable = 100'000'000'0000ll;
   const int64_t claimable = int64_t(max_claimable * double(current_time_point().sec_since_epoch()-base_time) / (10*seconds_per_year) );

   check( max_claimable - claimable <= stake, "b1 can only claim their tokens over 10 years" );
}

void system_contract::changebw( name from, name receiver,
                                const asset stake_net_delta, const asset stake_cpu_delta, bool transfer )
{
   require_auth( from );
   check( stake_net_delta.amount != 0 || stake_cpu_delta.amount != 0, "should stake non-zero amount" );
   check( std::abs( (stake_net_delta + stake_cpu_delta).amount )
          >= std::max( std::abs( stake_net_delta.amount ), std::abs( stake_cpu_delta.amount ) ),
          "net and cpu deltas cannot be opposite signs" );

   name source_stake_from = from;
   if ( transfer ) {
      from = receiver;
   }

   // update stake delegated from "from" to "receiver"
   {
      del_bandwidth_table     del_tbl( _self, from.value );
      auto itr = del_tbl.find( receiver.value );
      if( itr == del_tbl.end() ) {
         itr = del_tbl.emplace( from, [&]( auto& dbo ){
            dbo.from          = from;
            dbo.to            = receiver;
            dbo.net_weight    = stake_net_delta;
            dbo.cpu_weight    = stake_cpu_delta;
         });
      }
      else {
         del_tbl.modify( itr, same_payer, [&]( auto& dbo ){
            dbo.net_weight    += stake_net_delta;
            dbo.cpu_weight    += stake_cpu_delta;
         });
      }
      check( 0 <= itr->net_weight.amount, "insufficient staked net bandwidth" );
      check( 0 <= itr->cpu_weight.amount, "insufficient staked cpu bandwidth" );
      if ( itr->is_empty() ) {
         del_tbl.erase( itr );
      }
   } // itr can be invalid, should go out of scope

   // update totals of "receiver"
   {
      user_resources_table   totals_tbl( _self, receiver.value );
      auto tot_itr = totals_tbl.find( receiver.value );
      if( tot_itr ==  totals_tbl.end() ) {
         tot_itr = totals_tbl.emplace( from, [&]( auto& tot ) {
            tot.owner = receiver;
            tot.net_weight    = stake_net_delta;
            tot.cpu_weight    = stake_cpu_delta;
         });
      } else {
         totals_tbl.modify( tot_itr, from == receiver ? from : same_payer, [&]( auto& tot ) {
            tot.net_weight    += stake_net_delta;
            tot.cpu_weight    += stake_cpu_delta;
         });
      }
      check( 0 <= tot_itr->net_weight.amount, "insufficient staked total net bandwidth" );
      check( 0 <= tot_itr->cpu_weight.amount, "insufficient staked total cpu bandwidth" );

      int64_t ram_bytes, net, cpu;
      get_resource_limits( receiver, ram_bytes, net, cpu );

      set_resource_limits( receiver, std::max( tot_itr->ram_bytes + ram_gift_bytes, ram_bytes ), tot_itr->net_weight.amount, tot_itr->cpu_weight.amount );

      if ( tot_itr->is_empty() ) {
         totals_tbl.erase( tot_itr );
      }
   } // tot_itr can be invalid, should go out of scope

   // create refund or update from existing refund
   if ( stake_account != source_stake_from ) { //for eosio both transfer and refund make no sense
      refunds_table refunds_tbl( _self, from.value );
      auto req = refunds_tbl.find( from.value );

      //create/update/delete refund
      auto net_balance = stake_net_delta;
      auto cpu_balance = stake_cpu_delta;
      bool need_deferred_trx = false;


      // net and cpu are same sign by assertions in delegatebw and undelegatebw
      // redundant assertion also at start of changebw to protect against misuse of changebw
      bool is_undelegating = (net_balance.amount + cpu_balance.amount ) < 0;
      bool is_delegating_to_self = (!transfer && from == receiver);

      if( is_delegating_to_self || is_undelegating ) {
         if ( req != refunds_tbl.end() ) { //need to update refund
            refunds_tbl.modify( req, same_payer, [&]( refund_request& r ) {
               if ( net_balance.amount < 0 || cpu_balance.amount < 0 ) {
                  r.request_time = current_time_point();
               }
               r.net_amount -= net_balance;
               if ( r.net_amount.amount < 0 ) {
                  net_balance = -r.net_amount;
                  r.net_amount.amount = 0;
               } else {
                  net_balance.amount = 0;
               }
               r.cpu_amount -= cpu_balance;
               if ( r.cpu_amount.amount < 0 ){
                  cpu_balance = -r.cpu_amount;
                  r.cpu_amount.amount = 0;
               } else {
                  cpu_balance.amount = 0;
               }
            });

            check( 0 <= req->net_amount.amount, "negative net refund amount" ); //should never happen
            check( 0 <= req->cpu_amount.amount, "negative cpu refund amount" ); //should never happen

            if ( req->is_empty() ) {
               refunds_tbl.erase( req );
               need_deferred_trx = false;
            } else {
               need_deferred_trx = true;
            }
         } else if ( net_balance.amount < 0 || cpu_balance.amount < 0 ) { //need to create refund
            refunds_tbl.emplace( from, [&]( refund_request& r ) {
               r.owner = from;
               if ( net_balance.amount < 0 ) {
                  r.net_amount = -net_balance;
                  net_balance.amount = 0;
               } else {
                  r.net_amount = asset( 0, core_symbol() );
               }
               if ( cpu_balance.amount < 0 ) {
                  r.cpu_amount = -cpu_balance;
                  cpu_balance.amount = 0;
               } else {
                  r.cpu_amount = asset( 0, core_symbol() );
               }
               r.request_time = current_time_point();
            });
            need_deferred_trx = true;
         } // else stake increase requested with no existing row in refunds_tbl -> nothing to do with refunds_tbl
      } /// end if is_delegating_to_self || is_undelegating

      if ( need_deferred_trx ) {
         eosio::transaction out;
         out.actions.emplace_back( permission_level{from, active_permission},
                                   _self, "refund"_n,
                                   from
         );
         out.delay_sec = refund_delay_sec;
         cancel_deferred( from.value ); // TODO: Remove this line when replacing deferred trxs is fixed
         out.send( from.value, from, true );
      } else {
         cancel_deferred( from.value );
      }

      auto transfer_amount = net_balance + cpu_balance;
      if ( 0 < transfer_amount.amount ) {
         INLINE_ACTION_SENDER(eosio::token, transfer)(
               token_account, { {source_stake_from, active_permission} },
               { source_stake_from, stake_account, asset(transfer_amount), std::string("stake bandwidth") }
         );
      }
   }

   update_voting_power( from, stake_net_delta + stake_cpu_delta );
}

void system_contract::update_voting_power( const name& voter, const asset& total_update )
{
   auto voter_itr = _voters.find( voter.value );
   if( voter_itr == _voters.end() ) {
      voter_itr = _voters.emplace( voter, [&]( auto& v ) {
         v.owner  = voter;
         v.staked = total_update.amount;
      });
   } else {
      _voters.modify( voter_itr, same_payer, [&]( auto& v ) {
         v.staked += total_update.amount;
      });
   }

   check( 0 <= voter_itr->staked, "stake for voting cannot be negative");

   if( voter == "b1"_n ) {
      validate_b1_vesting( voter_itr->staked );
   }

   if( voter_itr->producers.size() || voter_itr->proxy ) {
      update_votes( voter, voter_itr->proxy, voter_itr->producers, false );
   }
}

void system_contract::delegatebw( name from, name receiver,
                                  asset stake_net_quantity,
                                  asset stake_cpu_quantity, bool transfer )
{
   asset zero_asset( 0, core_symbol() );
   check( stake_cpu_quantity >= zero_asset, "must stake a positive amount" );
   check( stake_net_quantity >= zero_asset, "must stake a positive amount" );
   check( stake_net_quantity.amount + stake_cpu_quantity.amount > 0, "must stake a positive amount" );
   check( !transfer || from != receiver, "cannot use transfer flag if delegating to self" );

   changebw( from, receiver, stake_net_quantity, stake_cpu_quantity, transfer);
} // delegatebw

void system_contract::undelegatebw( name from, name receiver,
                                    asset unstake_net_quantity, asset unstake_cpu_quantity )
{
   asset zero_asset( 0, core_symbol() );
   check( unstake_cpu_quantity >= zero_asset, "must unstake a positive amount" );
   check( unstake_net_quantity >= zero_asset, "must unstake a positive amount" );
   check( unstake_cpu_quantity.amount + unstake_net_quantity.amount > 0, "must unstake a positive amount" );
   check( _gstate.total_activated_stake >= min_activated_stake,
          "cannot undelegate bandwidth until the chain is activated (at least 15% of all tokens participate in voting)" );

   changebw( from, receiver, -unstake_net_quantity, -unstake_cpu_quantity, false);
} // undelegatebw


void system_contract::refund( const name owner ) {
   require_auth( owner );

   refunds_table refunds_tbl( _self, owner.value );
   auto req = refunds_tbl.find( owner.value );
   check( req != refunds_tbl.end(), "refund request not found" );
   check( req->request_time + seconds(refund_delay_sec) <= current_time_point(),
          "refund is not available yet" );

   INLINE_ACTION_SENDER(eosio::token, transfer)(
         token_account, { {stake_account, active_permission}, {req->owner, active_permission} },
         { stake_account, req->owner, req->net_amount + req->cpu_amount, std::string("unstake") }
   );

   refunds_tbl.erase( req );
}


} //namespace eosiosystem
